package com.hzw.saas.web.storage.controller;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.hzw.saas.api.storage.*;
import com.hzw.saas.api.storage.bo.*;
import com.hzw.saas.api.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.storage.enums.ResourceAuthEnum;
import com.hzw.saas.api.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.storage.enums.StorageSpaceEnum;
import com.hzw.saas.api.storage.exception.FileConflictException;
import com.hzw.saas.common.config.annotation.SysLog;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.query.LogicQuery;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.security.utils.SecurityUtils;
import com.hzw.saas.common.util.HttpContextUtils;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.enums.AssertEnum;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.service.storage.annotation.FileAuthAfter;
import com.hzw.saas.service.storage.annotation.FileAuthBefore;
import com.hzw.saas.service.storage.annotation.FolderAuthBefore;
import com.hzw.saas.service.storage.config.StorageConfig;
import com.hzw.saas.service.storage.util.ImageUtil;
import com.hzw.saas.service.storage.util.StorageUtil;
import com.hzw.saas.web.storage.dto.ResourceFilePostDTO;
import com.hzw.saas.web.storage.dto.ResourceFolderPostDTO;
import com.hzw.saas.web.storage.param.*;
import io.swagger.annotations.*;
import jodd.io.FileNameUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;

/**
 * @author zzl
 * @since 07/04/2021
 */
@Controller
@Validated
@RequiredArgsConstructor
@Slf4j
@Api(tags = "资源管理/个人资源")
public class UserResourceController {

    private final HttpServletRequest request;
    private final HttpServletResponse response;

    private final StorageConfig storageConfig;

    private final IStorageService storageService;
    private final IResourceService resourceService;
    private final IResourceFileService fileService;
    private final IResourceFolderService folderService;
    private final InxChunkService chunkService;

    private final IThumbnailService thumbnailService;

    @GetMapping("/resource/folder/{folderId}/children")
    @FolderAuthBefore(folderId = "#folderId", auth = ResourceAuthEnum.OWN)
    @SysLog
    @Transactional(rollbackFor = Exception.class, timeout = 10)
    @ApiOperation(value = "获取目录结构", notes = "获取目录结构，即目录中的子目录和子文件列表")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "folderId", value = "父目录ID，根目录ID为对应存储空间ID; " + StorageSpaceEnum.API_TEXT, dataType = "String", required = true),
    })
    @ApiOperationSort(11)
    public ResponseEntity<ResourcesBO> listSubResources(@PathVariable String folderId, ResourcesGetParam param) {
        // 设置参数
        String userId = SecurityUtils.getUser().getUserId();
        folderId = StorageUtil.getProperFolderId(userId, folderId);
        int pathLevel = Objects.isNull(param.getPathLevel()) ? 1 : param.getPathLevel();
        boolean withFile = Objects.equals(param.getWithFile(), true);
        LogicQuery<Integer> status = Objects.nonNull(param.getSyncMode()) && param.getSyncMode() > 0 ?
            LogicQuery.none() : LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode());
        ResourcesBO resources = resourceService.listSubResources(folderId, status, pathLevel, withFile);
        return ResponseEntity.ok(resources);
    }

    @GetMapping("/resource/recycle/folder/{folderId}/children")
    @FolderAuthBefore(folderId = "#folderId", auth = ResourceAuthEnum.OWN)
    @SysLog
    @ApiOperation(value = "获取回收站目录结构", notes = "获取回收站目录结构")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "folderId", value = "父目录ID，根目录ID为对应存储空间ID; " + StorageSpaceEnum.API_TEXT, dataType = "String", required = true),
    })
    @ApiOperationSort(12)
    public ResponseEntity<ResourcesBO> listRecycledResources(@PathVariable String folderId) {
        // 设置参数
        String userId = SecurityUtils.getUser().getUserId();
        List<ResourceFolderStructBO> folders;
        List<ResourceFileBO> files;
        if (StorageUtil.isSpaceId(folderId)) {
            folders = folderService.listUserFolders(userId, folderId,
                LogicQuery.eq(ResourceStatusEnum.RECYCLED.getCode()));
            files = fileService.listUserFiles(userId, folderId,
                LogicQuery.eq(ResourceStatusEnum.RECYCLED.getCode()),
                LogicQuery.none());
        } else {
            folders = folderService.listSubFolders(folderId,
                LogicQuery.eq(ResourceStatusEnum.RECYCLED_LINKED.getCode()));
            files = fileService.listSubFiles(folderId,
                LogicQuery.eq(ResourceStatusEnum.RECYCLED_LINKED.getCode()),
                LogicQuery.none());
        }
        return ResponseEntity.ok(new ResourcesBO(folders, files));
    }

    @PostMapping("/resource/folders")
    @FolderAuthBefore(folderId = "#param.supId", auth = ResourceAuthEnum.OWN)
    @SysLog
    @ApiOperation(value = "创建资源目录", notes = "创建目录，支持多级目录创建")
    @ApiOperationSort(13)
    public ResponseEntity<List<ResourceFolderPostDTO>> createFolders(@Validated @RequestBody ResourceFolderPostParam param) {
        String userId = SecurityUtils.getUser().getUserId();
        param.setSupId(StorageUtil.getProperFolderId(userId, param.getSupId()));
        List<ResourceFolderStructBO> foldersByPath = folderService.createFoldersByPath(param.getSupId(), param.getPath());
        List<ResourceFolderPostDTO> result = MapperUtil.nf().mapAsList(foldersByPath, ResourceFolderPostDTO.class);
        return ResponseEntity.ok(result);
    }

    @PutMapping("/resource/folder/name")
    @FolderAuthBefore(folderId = "#param.pid", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "重命名目录", mode = 3)
    @ApiOperation(value = "重命名目录", notes = "重命名目录")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名目录"),
    })
    @ApiOperationSort(14)
    public ResponseEntity<Void> renameFolder(@Validated @RequestBody ResourceRenameParam param) {
        try {
            this.folderService.renameFolder(param.getPid(), param.getName(), ConflictModeEnum.THROW_ERR);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @PutMapping("/resource/folder/path")
    @FolderAuthBefore(folderId = "#param.pid", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "移动目录", mode = 3)
    @ApiOperation(value = "移动目录", notes = "移动目录，与批量移动不同的是，该接口可以指定移动后的新名称")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名目录"),
    })
    @ApiOperationSort(15)
    public ResponseEntity<Void> moveFolder(@Validated @RequestBody ResourceMoveParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            // 移动
            this.folderService.moveFolder(param.getPid(), param.getTarSupId(), param.getNewName(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @PostMapping("/resource/folder/path")
    @FolderAuthBefore(folderId = "#param.pid", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "复制目录", mode = 3)
    @ApiOperation(value = "复制目录", notes = "复制目录，与批量复制不同的是，该接口可以指定复制后的新名称")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名目录"),
    })
    @ApiOperationSort(16)
    public ResponseEntity<Void> copyFolder(@Validated @RequestBody ResourceMoveParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            String spaceId = this.getSpaceId(param.getTarSupId());
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            // 检查容量
            storageService.assertNotOverSize(userId, spaceId, null, ListUtil.list(false, param.getPid()));
            // 复制
            this.folderService.copyFolder(param.getPid(), param.getTarSupId(), param.getNewName(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }


    @GetMapping("/resource/file/{fileId}/info")
    @SysLog
    @FileAuthAfter(auth = ResourceAuthEnum.READ)
    @ApiOperation(value = "获取文件信息", notes = "获取文件详细信息")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "fileId", value = "资源文件ID", dataType = "String", required = true)
    })
    @ApiOperationSort(21)
    public ResponseEntity<ResourceFileBO> getFileInfo(@PathVariable String fileId) {
        ResourceFileBO file = fileService.getFile(fileId, AssertEnum.FOUND);
        resourceService.assembleBaseInfo(file);
        return ResponseEntity.ok(file);
    }

    @PostMapping("/resource/file/info")
    @FolderAuthBefore(folderId = "#param.supId", auth = ResourceAuthEnum.OWN)
    @SysLog
    @ApiOperation(value = "提交文件信息", notes = "提交资源文件信息，用于分片上传资源前获取服务器返回的资源唯一标识符(fileId)，若存在同名文件，当覆盖模式为false时，会返回409错误，当覆盖模式为true时，会覆盖同名文件")
    @ApiResponses({
        @ApiResponse(code = 200, message = "返回资源标识ID"),
        @ApiResponse(code = 400, message = "请求参数错误"),
        @ApiResponse(code = 403, message = "无访问权限"),
        @ApiResponse(code = 409, message = "存在同名文件"),
        @ApiResponse(code = 413, message = "单文件大小超过1G"),
        @ApiResponse(code = 423, message = "存储总容量不足"),
    })
    @ApiOperationSort(22)
    public ResponseEntity<ResourceFilePostDTO> saveFileInfo(@Validated @RequestBody ResourceFilePostParam param) {
        log.debug("post resource info request...");
        // 操作用户
        String userId = SecurityUtils.getUser().getUserId();
        // 设置参数
        String spaceId = this.getSpaceId(param.getSupId());
        param.setSupId(StorageUtil.getProperFolderId(userId, param.getSupId()));
        // 判断文件大小是否超出
        this.assertNotOverLimitSize(param.getLength());
        // 判断用户存储空间是否足够
        storageService.assertNotOverSize(userId, spaceId, param.getLength());
        // 保存信息
        ResourceFileStorageBO fileInfo = MapperUtil.nf().map(param, ResourceFileStorageBO.class);
        fileInfo.setCreatorId(userId);
        fileInfo.setEditorId(userId);
        fileInfo.setCatalog(storageConfig.getPersonalSpaceCatalog());
        fileInfo.setIntegrity(FileIntegrityEnum.UPLOADING.getCode());
        try {
            ConflictModeEnum conflictModeEnum = Objects.equals(param.getCover(), true) ? ConflictModeEnum.COVER_TAR_PURE : ConflictModeEnum.THROW_ERR;
            ResourceFileBO result = fileService.saveFile(fileInfo, conflictModeEnum);
            // 返回
            ResourceFilePostDTO resourceFilePostDto = new ResourceFilePostDTO(result.getPid(), result.getVid(), result.getPid());
            return ResponseEntity.ok(resourceFilePostDto);
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    /**
     * 单独校验每个分片
     */
    @GetMapping("/resource/file/inx-chunk")
    @SysLog
    @ApiOperation(value = "校验文件分片", notes = "用于分片插件开启分片上传校验时使用，如vue-simple-uploader中开启testChunks=true")
    @ApiResponses({
        @ApiResponse(code = 200, message = "分片已上传过"),
        @ApiResponse(code = 304, message = "分片未上传过"),
        @ApiResponse(code = 400, message = "请求参数异常"),
        @ApiResponse(code = 500, message = "服务器异常")
    })
    @ApiOperationSort(23)
    public ResponseEntity<Void> checkChunk(@Validated InxChunkBO chunkCheck) {
        log.debug("chunk check request...");
        // 设置分片校验状态信息
        // 默认返回非(200, 201, 202)状态码，正常走标准上传(不进行分片校验)
        int status = HttpServletResponse.SC_NOT_MODIFIED;
        // 检查上传状态
        boolean exist = chunkService.exist(storageConfig.getPersonalSpaceCacheCatalog(), chunkCheck);
        // 文件上传完成，可跳过上传（秒传）
        if (exist) {
            status = HttpServletResponse.SC_OK;
        }
        return ResponseEntity.status(status).build();
    }

    @PostMapping(value = "/resource/file/inx-chunk", consumes = {"multipart/form-data"})
    @SysLog
    @ApiOperation(value = "上传文件分片", notes = "上传文件分片，该接口支持同一分片重复调用上传。上传文件前请先调用<提交个人空间文件信息>接口，获取服务器提供的文件ID")
    @ApiResponses({
        @ApiResponse(code = 200, message = "上传成功"),
        @ApiResponse(code = 400, message = "请求参数或请求类型异常"),
        @ApiResponse(code = 415, message = "文件类型不支持"),
        @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @ApiOperationSort(24)
    public ResponseEntity<Boolean> uploadChunk(@Validated InxChunkDataBO param, HttpServletRequest request) {
        log.debug("chunk upload request...");
        // 判断请求类型
        boolean multipart = ServletUtil.isMultipart(request);
        AssertUtil.assertThrow("上传失败，请求参数异常，请使用multipart/form-data类型上传", !multipart);
        try {
            // 此处直接将spring获取到的缓存文件放进内存
            // 这一步是为了防止后续在异步处理分片文件保存时，springboot将缓存文件删除
            // 分片保存
            byte[] data = param.getChunkFile().getBytes();
            chunkService.saveChunk(storageConfig.getPersonalSpaceCacheCatalog(), param, data);
            return ResponseEntity.ok(true);
        } catch (IOException e) {
            log.error("chunk upload error", e);
            throw DefaultSaasException.build(e.getMessage());
        }
    }

    @PutMapping("/resource/file/inx-chunks")
    @FileAuthBefore(fileId = "#param.fileId", auth = ResourceAuthEnum.OWN)
    @SysLog
    @ApiOperation(value = "完成文件上传", notes = "文件分片上传完成后执行该回调方法")
    @ApiOperationSort(25)
    public ResponseEntity<Boolean> mergeChunks(@Validated @RequestBody ResourceFileMergeParam param) {
        // 合并分片
        chunkService.mergeChunks(param.getFileId(), storageConfig.getPersonalSpaceCacheCatalog(), param.getStorageMode());
        return ResponseEntity.ok(true);
    }

    @DeleteMapping("/resource/file/{fileId}/chunks")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "取消文件上传", mode = 3)
    @ApiOperation(value = "取消文件上传", notes = "取消文件上传，删除服务端已接收的文件分片和文件信息")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "fileId", value = "资源文件ID", required = true, dataType = "String")
    })
    @ApiResponses({
        @ApiResponse(code = 200, message = "请求成功"),
        @ApiResponse(code = 500, message = "服务器内部错误"),
    })
    @ApiOperationSort(26)
    public ResponseEntity<Void> cancelUpload(@NotBlank(message = "资源ID不可为空") @PathVariable String fileId) {
        ResourceFileBO file = fileService.getFile(fileId, AssertEnum.IGNORE);
        if (Objects.nonNull(file))
            return ResponseEntity.ok().build();
        this.fileService.deleteFile(fileId);
        this.chunkService.deleteChunks(fileId, storageConfig.getPersonalSpaceCacheCatalog());
        return ResponseEntity.ok().build();
    }

    @PutMapping("/resource/file/name")
    @FileAuthBefore(fileId = "#param.pid", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "重命名文件", mode = 3)
    @ApiOperation(value = "重命名文件", notes = "重命名文件")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名文件"),
    })
    @ApiOperationSort(27)
    public ResponseEntity<Void> renameFile(@Validated @RequestBody ResourceRenameParam param) {
        try {
            this.fileService.renameFile(param.getPid(), param.getName(), ConflictModeEnum.THROW_ERR);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @PutMapping("/resource/file/path")
    @FileAuthBefore(fileId = "#param.pid", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderId = "#param.tarSupId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "移动文件", mode = 3)
    @ApiOperation(value = "移动文件", notes = "移动文件，与批量移动不同的是，该接口可以指定移动后的新名称")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名文件"),
    })
    @ApiOperationSort(28)
    public ResponseEntity<Void> moveFile(@Validated @RequestBody ResourceMoveParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            // 移动
            this.fileService.moveFile(param.getPid(), param.getTarSupId(), param.getNewName(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }


    @PostMapping("/resource/file/path")
    @FileAuthBefore(fileId = "#param.pid", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderId = "#param.tarSupId", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "复制文件", mode = 3)
    @ApiOperation(value = "复制文件", notes = "复制文件，与批量复制不同的是，该接口可以指定复制后的新名称")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名文件"),
        @ApiResponse(code = 423, message = "存储总容量不足"),
    })
    @ApiOperationSort(29)
    public ResponseEntity<Void> copyFile(@Validated @RequestBody ResourceMoveParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            String spaceId = this.getSpaceId(param.getTarSupId());
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            // 检查容量
            storageService.assertNotOverSize(userId, spaceId, ListUtil.list(false, param.getPid()), null);
            // 复制
            this.fileService.copyFile(param.getPid(), param.getTarSupId(), param.getNewName(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }


    @PutMapping("/resources/path")
    @FileAuthBefore(fileIds = "#param.fileIds", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderId = "#param.tarSupId", folderIds = "#param.folderIds", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "批量移动", mode = 3)
    @ApiOperation(value = "批量移动", notes = "批量移动目录/文件")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名文件"),
    })
    @ApiOperationSort(31)
    public ResponseEntity<Void> moveResources(@Validated @RequestBody ResourcesMoveParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            // 移动
            resourceService.moveResources(param.getFolderIds(), param.getFileIds(), param.getTarSupId(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @PostMapping("/resources/path")
    @FileAuthBefore(fileIds = "#param.fileIds", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderId = "#param.tarSupId", folderIds = "#param.folderIds", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "批量复制", mode = 3)
    @ApiOperation(value = "批量复制", notes = "批量复制目录/文件")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名文件"),
        @ApiResponse(code = 423, message = "存储总容量不足"),
    })
    @ApiOperationSort(32)
    public ResponseEntity<Void> copyResources(@Validated @RequestBody ResourcesMoveParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            String spaceId = this.getSpaceId(param.getTarSupId());
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            // 检查容量
            storageService.assertNotOverSize(userId, spaceId, param.getFileIds(), param.getFolderIds());
            // 复制
            resourceService.copyResources(param.getFolderIds(), param.getFileIds(), param.getTarSupId(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @DeleteMapping("/resources/status")
    @FileAuthBefore(fileIds = "#param.fileIds", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderIds = "#param.folderIds", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "回收资源", mode = 3)
    @ApiOperation(value = "回收资源", notes = "回收资源，支持批量")
    @ApiOperationSort(33)
    public ResponseEntity<Void> recycleResources(@Validated @RequestBody ResourcesDeleteParam param) {
        resourceService.recycleResources(param.getFolderIds(), param.getFileIds());
        return ResponseEntity.ok().build();
    }

    @PutMapping("/resources/status")
    @FileAuthBefore(fileIds = "#param.fileIds", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderId = "#param.tarSupId", folderIds = "#param.folderIds", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "恢复资源", mode = 3)
    @ApiOperation(value = "恢复资源", notes = "恢复资源，支持批量")
    @ApiResponses({
        @ApiResponse(code = 200, message = "更新成功"),
        @ApiResponse(code = 409, message = "存在同名文件"),
    })
    @ApiOperationSort(34)
    public ResponseEntity<Void> restoreResources(@Validated @RequestBody ResourcesRestoreParam param) {
        try {
            String userId = SecurityUtils.getUser().getUserId();
            // 设置参数
            param.setTarSupId(StorageUtil.getProperFolderId(userId, param.getTarSupId()));
            ConflictModeEnum conflictMode = ConflictModeEnum.instance(Objects.equals(param.getCover(), true));
            resourceService.restoreResources(param.getFolderIds(), param.getFileIds(), param.getTarSupId(), conflictMode);
            return ResponseEntity.ok().build();
        } catch (FileConflictException e) {
            throw DefaultSaasException.build(e.getMessage()).status(HttpStatus.CONFLICT);
        }
    }

    @DeleteMapping("/resources")
    @FileAuthBefore(fileIds = "#param.fileIds", auth = ResourceAuthEnum.OWN)
    @FolderAuthBefore(folderIds = "#param.folderIds", auth = ResourceAuthEnum.OWN)
    @SysLog(operation = "删除资源", mode = 3)
    @ApiOperation(value = "删除资源", notes = "删除资源，支持批量")
    @ApiOperationSort(35)
    public ResponseEntity<Void> deleteResources(@Validated @RequestBody ResourcesDeleteParam param) {
        resourceService.deleteResources(param.getFolderIds(), param.getFileIds());
        return ResponseEntity.ok().build();
    }

    @GetMapping("/resource/file/{fileId}")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.READ)
    @SysLog(operation = "下载文件", mode = 3)
    @ApiOperation(value = "下载文件", notes = "下载个人空间文件")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "access_token", value = "用户token（或在请求头设置Authorization）", dataType = "String", paramType = "query", required = false),
        @ApiImplicitParam(name = "fileId", value = "资源文件ID", dataType = "String", required = true),
        @ApiImplicitParam(name = "startBytes", value = "起始字节数，如501，即为从501字节处开始下载", dataType = "Int", required = false),
        @ApiImplicitParam(name = "fileName", value = "自定义文件名", dataType = "String", required = false)
    })
    @ApiOperationSort(41)
    public void downloadFile(@PathVariable String fileId, Long startBytes, String fileName) {
        log.debug("download request...");
        ResourceFileBO resourceFileBo = fileService.getFile(fileId, AssertEnum.FOUND);
        try (OutputStream fos = response.getOutputStream()) {
            startBytes = Objects.isNull(startBytes) || (startBytes.compareTo(0L) <= 0) ? 1L : startBytes;
            log.debug("start download file: {}, start: {}", resourceFileBo.getPid(), startBytes);
            fileName = StrUtil.isBlank(fileName) ? resourceFileBo.getName() : fileName;
            HttpContextUtils.setFileDownloadHeader(request, response, fileName, resourceFileBo.getLength() - (startBytes - 1L));
            fileService.downloadFileData(fileId, fos, startBytes);
            response.flushBuffer();
            log.debug("download file over...");
        } catch (IOException e) {
            log.error("download error", e);
            throw DefaultSaasException.build("下载文件异常");
        }
    }

    @GetMapping("/resource/file/{fileId}/thumbnail")
    @FileAuthBefore(fileId = "#fileId", auth = ResourceAuthEnum.READ)
    @SysLog(operation = "下载文件缩略图", mode = 3)
    @ApiOperation(value = "下载文件缩略图", notes = "下载文件缩略图")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "access_token", value = "用户token（或在请求头设置Authorization）", dataType = "String", paramType = "query", required = false),
        @ApiImplicitParam(name = "fileId", value = "资源文件ID", dataType = "String", required = true),
    })
    @ApiOperationSort(41)
    public void downloadThumbnail(@PathVariable String fileId, Integer thumbnailSize) {
        log.debug("download thumbnail request...");
        ResourceFileStorageBO fileInfo = fileService.getFileWithStorage(fileId, LogicQuery.ne(ResourceStatusEnum.DELETED.groupCodes()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()), AssertEnum.EXIST);
        try (OutputStream fos = response.getOutputStream()) {
            String extension = FileNameUtil.getExtension(fileInfo.getName());
            if (extension == null) {
                extension = "";
            }
            extension = ImageUtil.imageTypes.contains(extension.toLowerCase()) ? extension : "jpg";
            String fileName = FileNameUtil.getBaseName(fileInfo.getName()) + "_thumbnail." + extension;
            HttpContextUtils.setImageHeader(request, response, 0, extension);
            thumbnailSize = thumbnailSize == null || thumbnailSize <= 0 ? storageConfig.getThumbnailSize() : thumbnailSize;
            thumbnailService.getThumbnail(fileInfo, fos, thumbnailSize);
            response.flushBuffer();
            log.debug("download file over...");
        } catch (IOException e) {
            log.error("download error", e);
            throw DefaultSaasException.build("下载文件异常");
        }
    }

    // ================================================ private method ================================================ //

    /**
     * 根据目录ID获取存储空间ID
     *
     * @param folderId
     * @return
     */
    private String getSpaceId(String folderId) {
        if (StorageUtil.isSpaceId(folderId))
            return folderId;
        if (StorageUtil.isRootId(folderId))
            return StorageUtil.parseSpaceIdByRootId(folderId);
        ResourceFolderStructBO folder = folderService.getFolder(folderId, AssertEnum.FOUND);
        return folder.getSpaceId();
    }

    /**
     * 单文件大小检查
     *
     * @param length
     * @return
     */
    private void assertNotOverLimitSize(long length) {
        AssertUtil.assertThrow("文件大小不可超过1G", HttpStatus.PAYLOAD_TOO_LARGE, length > storageConfig.getFileMaxSize());
    }

}
